#pragma once
#include "BufferAttribute.h"
#include <string>
#include <map>
#include <limits>
#include "../core/RenderDataCacheHelper.h"
POCELAIN_NAMESPACE_BEGIN
class BufferGeometry : public Object , public RenderDataCacheHelper
{
public:
    friend IRenderer;
    typedef BufferGeometry type;
    typedef UInt32BufferAttribute index_array_t;
    virtual bool isGeometry() const override{return true;}
    virtual EObjectClass getObjectClass() const override {return EObjectClass::E_BUFFER_GEOMETRY;}
    struct draw_range_t
    {
        size_t start;
        size_t count;
        draw_range_t(size_t start = 0 , size_t count = std::numeric_limits<size_t>::max())
            :start(start),
             count(count)
        {
        }
    };
    struct group_t
    {
        size_t start;
        size_t count;
        size_t materialIndex;
        group_t(size_t start = 0 , size_t count = 0 , size_t materialIndex =0)
            :start(start),
             count(count),
             materialIndex(materialIndex)
        {
        }
    };
    typedef std::vector<group_t> groups_t;

    groups_t mGroups;

    draw_range_t mDrawRange;
    BufferGeometry()
    {
    }
    virtual ~BufferGeometry()
    {
        if(mBoundingBox){
            delete mBoundingBox;
        }
        if(mBoundingSphere){
            delete mBoundingSphere;
        }
        for(auto i : mAttributes)
        {
            i.second->release();
        }
        if(mIndices)
        {
            mIndices->release();
        }
    }

    index_array_t *getIndices()
    {
        return this->mIndices;
    }

    void setIndices( index_array_t *index )
    {

        POCELAIN_ASSERT(index);
        if(mIndices)
        {
            mIndices->release();
        }
        index->retain();
        mIndices = index;

    }

    /* this.morphAttributes = {}; */
    /* this.morphTargetsRelative = false; */


    /* this.userData = {}; */

    bool hasAttributes(const std::string &name)const
    {
        return mAttributes.end() == mAttributes.find(name);
    }

    const std::map<std::string,IBufferAttribute*> &getNameAttributeMap()const
    {
        return mAttributes;
    }

    IBufferAttribute *getAttribute(const std::string &name )
    {

        IBufferAttribute *ret = nullptr;

        if(hasAttribute(name))
        {
            ret = mAttributes[ name ];
        }

        return ret;

    }

    type &setAttribute(const std::string &name,IBufferAttribute *attribute )
    {
        POCELAIN_ASSERT(attribute);

        attribute->retain();

        this->mAttributes[ name ] = attribute;

        return *this;

    }

    type &deleteAttribute(const std::string &name )
    {
        //TODO
        delete this->mAttributes[ name ];
        return *this;
    }
    void addGroup(size_t start,size_t count,size_t materialIndex = 0 )
    {
        this->mGroups.push_back(group_t{
                start,
                count,
                materialIndex
            } );
    }

    void clearGroups()
    {

        this->mGroups.clear();
    }

    void setDrawRange(size_t start,size_t count )
    {

        this->mDrawRange.start = start;
        this->mDrawRange.count = count;

    }
    bool hasAttribute (std::string name )
    {
        return mAttributes.find(name) != mAttributes.end();
    }

    type &applyMatrix4(const Matrix4 &matrix )
    {
        if(mAttributes.find("position") != mAttributes.end())
        {
            const auto &position = dynamic_cast<Float32BufferAttribute*>(mAttributes["position"]);
            position->applyMatrix4( matrix );
            position->needsUpdate = true;
        }

        if(mAttributes.find("normal") != mAttributes.end())
        {
            const auto &normal = dynamic_cast<Float32BufferAttribute*>(mAttributes["normal"]);
            const auto &normalMatrix = Matrix3().getNormalMatrix( matrix );
            normal->applyNormalMatrix( normalMatrix );
            normal->needsUpdate = true;
        }
        if(mAttributes.find("tangent") != mAttributes.end())
        {
            const auto &tangent = dynamic_cast<Float32BufferAttribute*>(mAttributes["tangent"]);
            tangent->transformDirection( matrix );
            tangent->needsUpdate = true;
        }
        if ( mBoundingBox != nullptr )
        {
            computeBoundingBox();
        }
        if ( mBoundingSphere != nullptr )
        {
            computeBoundingSphere();
        }
        return *this;
    }

    type &translate(Real x,Real y,Real z )
    {

        Matrix4 _m1;

        _m1.makeTranslation( x, y, z );

        applyMatrix4( _m1 );

        return *this;

    }

    type &center()
    {

        Vector3 _offset;

        computeBoundingBox();

        mBoundingBox->getCenter( _offset ).negate();

        translate( _offset.x, _offset.y, _offset.z );

        return *this;

    }
    // type &setFromPoints(const std::vector<Real> &points )
    // {

    // 	const position = [];

    // 	for ( size_t i = 0, l = points.size(); i < l; ++i ) {

    // 		const point = points[ i ];
    // 		position.push( point.x, point.y, point.z || 0 );

    // 	}

    // 	this.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );

    // 	return *this;

    // }

    void computeBoundingBox()
    {

        if ( this->mBoundingBox == nullptr ) {

            this->mBoundingBox = new Box3();

        }

        const auto &position = dynamic_cast<Float32BufferAttribute*>(this->mAttributes["position"]);
        // const auto &morphAttributesPosition = this.morphAttributes.position;

        // if ( position && position.isGLBufferAttribute ) {

        // 	console.error( 'THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box. Alternatively set "mesh.frustumCulled" to "false".', this );

        // 	this->mBoundingBox.set(
        // 		new Vector3( - Infinity, - Infinity, - Infinity ),
        // 		new Vector3( + Infinity, + Infinity, + Infinity )
        // 	);

        // 	return;

        // }

        if ( position != nullptr )
        {

            *this->mBoundingBox = position->getBoundingBox();

            // process morph attributes if present

            // if ( morphAttributesPosition ) {

            // 	for ( size_t i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

            // 		const morphAttribute = morphAttributesPosition[ i ];
            // 		_box.setFromBufferAttribute( morphAttribute );

            // 		if ( this.morphTargetsRelative ) {

            // 			_vector.addVectors( this.boundingBox.min, _box.min );
            // 			this.boundingBox.expandByPoint( _vector );

            // 			_vector.addVectors( this.boundingBox.max, _box.max );
            // 			this.boundingBox.expandByPoint( _vector );

            // 		} else {

            // 			this.boundingBox.expandByPoint( _box.min );
            // 			this.boundingBox.expandByPoint( _box.max );

            // 		}

            // 	}

            // }

        } else {

            this->mBoundingBox->makeEmpty();

        }

        // if ( isNaN( this->mBoundingBox->min.x ) || isNaN( this->mBoundingBox->min.y ) || isNaN( this->mBoundingBox->min.z ) ) {

        // 	console.error( 'THREE.BufferGeometry.computeBoundingBox(): Computed min/max have NaN values. The "position" attribute is likely to have NaN values.', this );

        // }

    }

    void computeBoundingSphere()
    {

        Vector3 _vector;
        Box3 _box;
        if ( this->mBoundingSphere == nullptr ) {

            this->mBoundingSphere = new Sphere();

        }

        const auto &position = dynamic_cast<Float32BufferAttribute*>(this->mAttributes["position"]);
        // const morphAttributesPosition = this.morphAttributes.position;

        // if ( position && position.isGLBufferAttribute ) {

        // 	console.error( 'THREE.BufferGeometry.computeBoundingSphere(): GLBufferAttribute requires a manual bounding sphere. Alternatively set "mesh.frustumCulled" to "false".', this );

        // 	this.boundingSphere.set( new Vector3(), Infinity );

        // 	return;

        // }

        if ( position ) {

            // first, find the center of the bounding sphere

            auto &center = this->mBoundingSphere->center;

            _box = position->getBoundingBox();

            // process morph attributes if present

            // if ( morphAttributesPosition )
            // {

            // 	for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

            // 		const morphAttribute = morphAttributesPosition[ i ];
            // 		_boxMorphTargets.setFromBufferAttribute( morphAttribute );

            // 		if ( this.morphTargetsRelative ) {

            // 			_vector.addVectors( _box.min, _boxMorphTargets.min );
            // 			_box.expandByPoint( _vector );

            // 			_vector.addVectors( _box.max, _boxMorphTargets.max );
            // 			_box.expandByPoint( _vector );

            // 		} else {

            // 			_box.expandByPoint( _boxMorphTargets.min );
            // 			_box.expandByPoint( _boxMorphTargets.max );

            // 		}

            // 	}

            // }

            center = _box.getCenter();

            // second, try to find a boundingSphere with a radius smaller than the
            // boundingSphere of the boundingBox: sqrt(3) smaller in the best case

            Real maxRadiusSq = 0;

            for ( size_t i = 0, il = position->count(); i < il; ++i )
            {
                _vector.set(
                            position->getX(i),
                            position->getY(i),
                            position->getZ(i)
                            );
                maxRadiusSq = std::max( maxRadiusSq, center.distanceToSquared( _vector ) );

            }

            // process morph attributes if present

            // if ( morphAttributesPosition ) {

            // 	for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

            // 		const morphAttribute = morphAttributesPosition[ i ];
            // 		const morphTargetsRelative = this.morphTargetsRelative;

            // 		for ( let j = 0, jl = morphAttribute.count; j < jl; j ++ ) {

            // 			_vector.fromBufferAttribute( morphAttribute, j );

            // 			if ( morphTargetsRelative ) {

            // 				_offset.fromBufferAttribute( position, j );
            // 				_vector.add( _offset );

            // 			}

            // 			maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) );

            // 		}

            // 	}

            // }

            this->mBoundingSphere->radius = std::sqrt( maxRadiusSq );

            // if ( isNaN( this.boundingSphere.radius ) ) {

            // 	console.error( 'THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.', this );

            // }

        }

    }

    void computeFaceNormals()
    {

        // backwards compatibility

    }

    void computeTangents()
    {

        //const index = this.index;
        //const attributes = this.attributes;

        // based on http://www.terathon.com/code/tangent.html
        // (per vertex tangents)

        //	if ( index === null ||
        //		 attributes.position === undefined ||
        //		 attributes.normal === undefined ||
        //		 attributes.uv === undefined ) {

        // 		console.error( 'THREE.BufferGeometry: .computeTangents() failed. Missing required attributes (index, position, normal or uv)' );
        // 		return;

        // 	}

        // 	const indices = index.array;
        // 	const positions = attributes.position.array;
        // 	const normals = attributes.normal.array;
        // 	const uvs = attributes.uv.array;

        // 	const nVertices = positions.length / 3;

        // 	if ( attributes.tangent === undefined ) {

        // 		this.setAttribute( 'tangent', new BufferAttribute( new Float32Array( 4 * nVertices ), 4 ) );

        // 	}

        // 	const tangents = attributes.tangent.array;

        // 	const tan1 = [], tan2 = [];

        // 	for ( let i = 0; i < nVertices; i ++ ) {

        // 		tan1[ i ] = new Vector3();
        // 		tan2[ i ] = new Vector3();

        // 	}

        // 	const vA = new Vector3(),
        // 		vB = new Vector3(),
        // 		vC = new Vector3(),

        // 		uvA = new Vector2(),
        // 		uvB = new Vector2(),
        // 		uvC = new Vector2(),

        // 		sdir = new Vector3(),
        // 		tdir = new Vector3();

        // 	function handleTriangle( a, b, c ) {

        // 		vA.fromArray( positions, a * 3 );
        // 		vB.fromArray( positions, b * 3 );
        // 		vC.fromArray( positions, c * 3 );

        // 		uvA.fromArray( uvs, a * 2 );
        // 		uvB.fromArray( uvs, b * 2 );
        // 		uvC.fromArray( uvs, c * 2 );

        // 		vB.sub( vA );
        // 		vC.sub( vA );

        // 		uvB.sub( uvA );
        // 		uvC.sub( uvA );

        // 		const r = 1.0 / ( uvB.x * uvC.y - uvC.x * uvB.y );

        // 		// silently ignore degenerate uv triangles having coincident or colinear vertices

        // 		if ( ! isFinite( r ) ) return;

        // 		sdir.copy( vB ).multiplyScalar( uvC.y ).addScaledVector( vC, - uvB.y ).multiplyScalar( r );
        // 		tdir.copy( vC ).multiplyScalar( uvB.x ).addScaledVector( vB, - uvC.x ).multiplyScalar( r );

        // 		tan1[ a ].add( sdir );
        // 		tan1[ b ].add( sdir );
        // 		tan1[ c ].add( sdir );

        // 		tan2[ a ].add( tdir );
        // 		tan2[ b ].add( tdir );
        // 		tan2[ c ].add( tdir );

        // 	}

        // 	let groups = this.groups;

        // 	if ( groups.length === 0 ) {

        // 		groups = [ {
        // 			start: 0,
        // 			count: indices.length
        // 		} ];

        // 	}

        // 	for ( let i = 0, il = groups.length; i < il; ++ i ) {

        // 		const group = groups[ i ];

        // 		const start = group.start;
        // 		const count = group.count;

        // 		for ( let j = start, jl = start + count; j < jl; j += 3 ) {

        // 			handleTriangle(
        // 				indices[ j + 0 ],
        // 				indices[ j + 1 ],
        // 				indices[ j + 2 ]
        // 			);

        // 		}

        // 	}

        // 	const tmp = new Vector3(), tmp2 = new Vector3();
        // 	const n = new Vector3(), n2 = new Vector3();

        // 	function handleVertex( v ) {

        // 		n.fromArray( normals, v * 3 );
        // 		n2.copy( n );

        // 		const t = tan1[ v ];

        // 		// Gram-Schmidt orthogonalize

        // 		tmp.copy( t );
        // 		tmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();

        // 		// Calculate handedness

        // 		tmp2.crossVectors( n2, t );
        // 		const test = tmp2.dot( tan2[ v ] );
        // 		const w = ( test < 0.0 ) ? - 1.0 : 1.0;

        // 		tangents[ v * 4 ] = tmp.x;
        // 		tangents[ v * 4 + 1 ] = tmp.y;
        // 		tangents[ v * 4 + 2 ] = tmp.z;
        // 		tangents[ v * 4 + 3 ] = w;

        // 	}

        // 	for ( let i = 0, il = groups.length; i < il; ++ i ) {

        // 		const group = groups[ i ];

        // 		const start = group.start;
        // 		const count = group.count;

        // 		for ( let j = start, jl = start + count; j < jl; j += 3 ) {

        // 			handleVertex( indices[ j + 0 ] );
        // 			handleVertex( indices[ j + 1 ] );
        // 			handleVertex( indices[ j + 2 ] );

        // 		}

        // 	}

    }

    void computeVertexNormals()
    {
        auto positionAttribute = dynamic_cast<Float32BufferAttribute*>(getAttribute( "position" ));

        if ( positionAttribute != nullptr )
        {

            auto normalAttribute = dynamic_cast<Float32BufferAttribute*>(getAttribute( "normal" ));

            if ( normalAttribute == nullptr )
            {
                normalAttribute = new Float32BufferAttribute( Float32BufferAttribute::array_t( positionAttribute->count() * 3 ), 3 );
                setAttribute( "normal", normalAttribute );

            } else {

                // reset existing normals to zero

                for ( size_t i = 0, il = normalAttribute->count(); i < il; i ++ )
                {

                    normalAttribute->setXYZ( i, 0, 0, 0 );

                }

            }

            Vector3 pA, pB, pC;
            Vector3 nA, nB, nC;
            Vector3 cb, ab;

            // indexed elements

            if ( mIndices ) {

                for ( size_t i = 0, il = mIndices->count(); i < il; i += 3 ) {

                    const auto vA = mIndices->getX( i + 0 );
                    const auto vB = mIndices->getX( i + 1 );
                    const auto vC = mIndices->getX( i + 2 );

                    pA = positionAttribute->toVector3(vA);
                    pB = positionAttribute->toVector3(vB);
                    pC = positionAttribute->toVector3(vC);

                    cb.subVectors( pC, pB );
                    ab.subVectors( pA, pB );
                    cb.cross( ab );

                    nA = normalAttribute->toVector3(vA);
                    nB = normalAttribute->toVector3(vB);
                    nC = normalAttribute->toVector3(vC);
                    //nA.fromBufferAttribute( normalAttribute, vA );
                    //nB.fromBufferAttribute( normalAttribute, vB );
                    //nC.fromBufferAttribute( normalAttribute, vC );

                    nA.add( cb );
                    nB.add( cb );
                    nC.add( cb );

                    normalAttribute->setXYZ( vA, nA.x, nA.y, nA.z );
                    normalAttribute->setXYZ( vB, nB.x, nB.y, nB.z );
                    normalAttribute->setXYZ( vC, nC.x, nC.y, nC.z );

                }

            } else {

                // non-indexed elements (unconnected triangle soup)

                for ( size_t i = 0, il = positionAttribute->count(); i < il; i += 3 ) {

                    pA= positionAttribute->toVector3(i + 0);
                    pB= positionAttribute->toVector3(i + 1);
                    pC= positionAttribute->toVector3(i + 2);

                    cb.subVectors( pC, pB );
                    ab.subVectors( pA, pB );
                    cb.cross( ab );

                    normalAttribute->setXYZ( i + 0, cb.x, cb.y, cb.z );
                    normalAttribute->setXYZ( i + 1, cb.x, cb.y, cb.z );
                    normalAttribute->setXYZ( i + 2, cb.x, cb.y, cb.z );

				}

			}

			this->normalizeNormals();

			normalAttribute->needsUpdate = true;

		}

	}

    void normalizeNormals()
    {
        Vector3 _vector;
		const auto &normals = dynamic_cast<Float32BufferAttribute*>(mAttributes["normal"]);

		for ( size_t i = 0, il = normals->count(); i < il; ++i )
        {

			_vector = normals->toVector3( i );
			_vector.normalize();
			normals->setXYZ( i, _vector.x, _vector.y, _vector.z );

		}

	}

    bool getDirty()const{return mDirty;}
private:
    
    std::map<std::string,IBufferAttribute*> mAttributes;
    Box3 *mBoundingBox = nullptr;
    Sphere *mBoundingSphere = nullptr;
    index_array_t *mIndices = nullptr;
    bool mDirty = true;
};
POCELAIN_NAMESPACE_END
